05. 응집도: 흩어져 있는 것들

📌 Contents

📌 static 메서드 오용

  • static 메서드로 정의하면 클래스의 인스턴스를 생성하지 않고 메서드를 호출할 수 있음

static 메서드는 인스턴스 변수를 사용할 수 없음

  • static 메서드는 인스턴스 변수를 사용할 수 없음
  • 따라서 어떤 메서드를 static 메서드로 만든 시점에 이미 데이터와 데이터를 조작하는 로직 사이에 괴리가 생김 -> 응집도가 낮아짐

인스턴스 변수를 사용하는 구조로 변경하기

  • '인스턴스 변수'와 '인스턴스 변수를 사용하는 로직'을 같은 클래스에 만드는 것이 응집도를 높이는 방법
  • 응집도가 높은 구조가 될 수 있도록 인스턴스 변수를 사용해서 계산하는 구조로 설계 하는 것이 좋음

인스턴스 메서드인 척하는 static 메서드 주의하기

  • static 메서드는 아니지만 static 메서드처럼 인스턴스 변수를 사용하지 않는 인스턴스 메서드들이 있음
  • 이러한 메서드를 구분하는 방법은 앞에 static을 붙여봤을 때 오류가 나는지 확인해 보면 됨

왜 static 메서드를 사용할까?

  • static 메서드를 사용하는 이유는 객체 지향 언어에서 C언어와 같은 절차 지향 언어의 접근 방법을 사용하려 하기 때문
  • 절차 지향 언어에서는 데이터와 로직이 따로 존재하도록 설계함
  • 이러한 접근 방법을 객체 지향 언어에 적용하여 설계하면, 데이터와 로직을 별도의 클래스에 배치하게 돼서 클래스의 인스턴스를 생성하지 않고도 사용할 수 있는 static 메서드를 활용하는 것임
  • static 메서드는 클래스의 인스턴스를 만들지 않아도 되므로, 간단하게 사용 가능하지만, 응집도가 낮아지는 문제가 있어 남용하지 않는 것이 좋음

어떠한 상황에서 static 메서드를 사용해야 좋을까?

  • 로그 출력 전용 메서드, 포맷 변환 전용 메서드처럼 응집도와 관계없는 기능은 static 메서드로 설계하는 것이 좋음
  • 뒤에 나올 팩토리 메서드도 static 메서드로 설계하는 것이 좋음

📌 초기화 로직 분산

  • 클래스를 잘 설계해도, 초기화 로직이 분산되어 응집도가 낮은 구조가 되어 버리는 경우가 있음
  • 생성자를 public으로 만들면, 의도하지 않은 용도로 사용될 수 있음
  • 결과적으로 관련된 로직이 분산됨
    ex) GiftPoint 클래스에서 신규 회원가입 시 주는 포인트를 매개변수로 받아 인스턴스를 생성한다면, 일반 가입, 프리미엄 가입 등에 따라 인스턴스 생성 시 생성자에 들어가는 매개변수 값이 달라지는데, 이 값을 바꾸고 싶을 때 GiftPoint 클래스만 보는 것이 아닌 코드 전체를 봐 인스턴스를 생성하는 부분을 확인해야함.


  • 이러한 초기화 로직의 분산을 막으려면 생성자를 private으로 만들고, 목적에 따라 팩토리 메서드를 만들어야함.
  • 생성자를 private으로 만들면, 클래스 내부에서만 인스턴스를 생성할 수 있음
  • 인스턴스를 생성하기 위한 static 팩토리 메서드에서 생성자를 호출함
  • 팩토리 메서드는 목적에 따라 만들어 두는 것이 일반적임
    ex) GiftPoint 클래스 안에 private static final int STANDARD_MEMBERSHIP_POINT = 3000;private static final int PREMIUM_MEMBERSHIP_POINT = 10000;를 선언하고

    static GiftPoint forStandardMembership() {
      return new GiftPoint(STANDARD_MEMBERSHIP_POINT);
    }
    
    static GiftPoint forPremiumMembership() {
      return new GiftPoint(PREMIUM_MEMBERSHIP_POINT);
    }
    

    메서드를 만듬.


  • 상황에 따라 생성 로직이 너무 많아 질 수 있음
  • 그러면 해당 클래스가 무엇을 하는 클래스인지 알기 어려워지기 때문에, 생성 전용 팩토리 클래스를 분리하는 방법을 고려하는 것이 좋음

📌 범용 처리 클래스(Common/Util)

  • 똑같은 일을 수행하는 코드가 많아지면 코드를 재사용하기 위해 범용 클래스를 만들곤 함 (Common, Util이라는 이름이 붙음)
  • 이때 static 메서드로 구현되는 경우가 많음


  • Common 클래스 안에 너무 많은 로직이 들어갈 수 있는데, 심지어 모두 static 메서드일 수 있는데, 원인은 Common과 Util이라는 이름이 '범용'이라는 뜻이기 때문에 사람들이 '범용적으로 사용하고 싶은 로직은 Common 클래스에 모아두면 되겠구나'라고 생각하게 만듬
  • 근본적인 원인은 범용의 의미와 재사용성을 잘못 이해하고 있기 때문임
  • 재사용성은 설계의 응집도를 높이면, 저절로 높아짐
  • 꼭 필요한 경우가 아니면, 범용 처리 클래스를 만들지 않는 것이 좋음 (세금 포함 금액 계산, 상품 주문하기 등은 해당 클래스에 로직이 있는 것이 좋음)


  • 로그 출력과 오류 확인은 애플리케이션의 모든 동작에 필요한 기능임
  • 온라인 쇼핑몰에서도 주문, 예약, 배송 같은 모든 상황에 필요한 기본 기능임
  • 이처럼 다양한 상황에서 넓게 활용되는 기능을 횡단 관심사(cross-cutting concern)라고 부름
    • 로그 출력
    • 오류 확인
    • 디버깅
    • 예외 처리
    • 캐시
    • 동기화
    • 분산 처리
  • 횡단 관심사에 해당하는 기능이라면 범용 코드로 만들어도 괜찮음

📌 결과를 리턴하는 데 매개변수 사용하지 않기

  • 결과 값을 매개변수로 받고 그걸 변경하는 것을 출력 매개변수라고 함
  • 출력 매개변수를 사용하면 응집도가 낮아짐
  • 결과 값을 매개변수로 받아 조작한다는 것은 데이터 조작 대상과 조작 로직이 각자 다른 클래스에 있는 것임. 즉, 응집도가 낮아져 코드 중복이 생길 수 있음
  • 출력 매개변수는 응집도 문제 이외에도 여러 문제를 발생시킴
  • 매개변수는 입력으로 전달하는 것이 일반적인데, 출력으로 사용해 버리면 매개변수가 입력인지 출력인지 메서드 내부의 로직을 확인해야 됨. 즉, 가독성이 떨어짐
  • c#에는 출력 매개변수임을 명시하는 out키워드와 ref키워드가 존재함

📌 매개변수가 너무 많은 경우

  • 너무 많은 매개변수를 받는 메서드는 실수로 잘못된 값을 대입할 가능성이 높음
  • 메서드의 매개변수를 전달한다는 것은 해당 매개변수를 사용해서 어떤 기능을 수행하고 싶다는 의미. 즉, 매개변수가 많다는 것은 많은 기능을 처리하고 싶다는 의미
  • 하지만 처리할 게 많아지면 로직이 복잡해지거나, 중복 코드가 생길 가능성이 높아짐. 여러 악마들이 모여 많은 문제를 일으킬 것임

기본 자료형에 대한 집착

  • boolean, int, float, double, String처럼 프로그래밍 언어가 표준적으로 제공하는 자료형을 기본 자료형(primitive type)이라고 함
  • 매개변수와 리턴 값에 모두 기본 자료형만 써 남용하는 현상을 기본 자료형 집착(primitive obsession)이라고 함
  • 기본 자료형으로만 구현하면, 중복 코드가 많이 생기고, 계산 로직이 이곳저곳에 분산되기 쉬움
  • 데이터는 단순히 존재하기만 할 수는 없음. 데이터를 사용해 계산하거나 데이터를 판단해서 제어 흐름을 전환할 때 사용
  • 기본 자료형으로만 구현하려고 하면, 데이터를 사용한 계산과 제어 로직이 모두 분산되어 응집도가 낮은 구조가 됨

의미 있는 단위는 모두 클래스로 만들기

  • 매개변수가 너무 많아지는 문제를 피하려면, 개념적으로 의미 있는 클래스를 만들어야 함 (값 객체 패턴)
  • 매개변수가 많으면 데이터 하나하나를 매개변수로 다루지 말고, 그 데이터를 인스턴스 변수로 갖는 클래스를 만들고 활용하는 설계로 변경해 보자!

📌 메서드 체인

  • .(점)으로 여러 메서드를 연결해서 리턴 값의 요소에 차례차례 접근하는 방법을 메서드 체인(열차 사고(train wreck)라고도 부름)이라고 부름
/**
* 갑옷 입기
* @param memberId 장비를 변경하고 싶은 멤버의 ID
* @param newArmor 입을 갑옷
*/
void equipArmor(int memberId, Armor newArmor) {
  if(party.members[memberId].equipments.canChange) {
    party.members[memberId].equipments.armor = newArmor;
  }
}
  • 이 방법은 응집도를 낮출 수 있어 좋지 않은 작성 방법임
  • armor, members,equipments 모두 모든 곳에서 요소에 접근할 수 있음
  • members,equipments, canChange, armor에 접근하는 코드가 여러 곳에 중복되어 구현되어 있다면, 이러한 요소의 사양이 조금이라도 변경되면, 해당 요소에 접근하고 있는 모든 코드를 확인하고 수정해야 함
  • 또 버그가 발생하면 어디에서 발생한 것인지 모든 코드를 확인해야함
  • 이러한 구조는 영향을 미치는 범위가 커질 수 있는 구조이므로, 전역 변수와 같은 성질을 갖음
  • 어디서든 아무 요소에나 접근할 수 있는 구조이므로, 사실 하나의 전역 변수보다 더 악질적임
  • '모르는 사람에게 말을 걸지 않기'라고 설명하기도 하는 객체 내부를 알아서는 안된다는 데메테르의 법칙이 있음
  • 메서드 체인으로 내부 구조를 돌아다닐 수 있는 설계는 데메테르의 법칙을 위반한다고 할 수 있음


  • 소프트웨어 설계에는 '묻지 말고, 명령하기(Tell, Don't Ask)'라는 유명한 격언이 있음
  • 이는 다른 객체의 내부 상태(변수)를 기반으로 판단하거나 제어하려고 하지 말고, 메서드로 명령해서 객체가 알아서 판단하고 제어하도록 설계하라는 의미임
  • 인스턴스 변수를 private으로 변경해서, 외부에서 접근할 수 없게 함
  • 그리고 인스턴스 변수에 대한 제어는 외부에서 메서드로 명령하는 형태로 만듬
  • 상세한 판단과 제어는 명령을 받는 쪽에서 담당하게 함

❓ Questions

❓ static 변수에 대해

  • static 변수에 대해 몇 가지 궁금증이 생겼다.


  • static 메서드에 대한 얘기는 책에 쭉 나오는데 그럼 static 변수도 메서드처럼 남용하면 안좋은 것인가?
  • 인스턴스 변수는 인스턴스가 생성될 때마다 독립적으로 생성되지만, static 변수는 해당 클래스에서 하나만 생성되고 클래스의 모든 인스턴스에서 공유된다고 한다.
  • 따라서 이를 변경하면 모든 인스턴스에 영향을 주므로 예기치 못한 부작용이 생길 수 있어 막 쓰면 안되고 필요한 상황에만 써야한다.


  • 그렇다면 static 변수는 주로 언제 쓰는가?
  • 팩토리 클래스 등 static 메서드를 써야할 때, static에서 사용할 변수를 static으로 선언해줘야 사용할 수 있다.
  • 따라서 static 메서드에서 사용할 변수에 static을 사용하면 되는 것 같다.
  • 옛날에 싱글턴 패턴에서도 인스턴스를 하나만 만들기 위해 static 메서드를 사용하는 것을 공부했었는데, 그 때 왜 instance라는 변수를 선언할 때 static으로 선언하나 궁금했었는데, 인스턴스를 생성하는 메서드가 static 메서드이기 때문에 그런 것이였다.


  • 이 외에도 static 변수의 특징들과 주로 사용하는 상황이 더 있는 것 같지만, 일단 이정도면 궁금증이 해소된 것 같다.
  • static 변수도 인스턴스 생성 없이 막 사용할 수 있어 편하다고 그냥 사용하면 안되고, 클래스의 모든 인스턴스에서 공유된다는 것을 인지하고, static 메서드와 함께 사용하는 것을 고려해서 적절한 상황에서 사용해야 하는 것 같다.

❓ 절차 지향 언어에서 데이터와 로직이 따로 존재하도록 설계하는 이유

  • 일단 절차 지향 언어에서 데이터 처리와 관리를 어떻게 하는지 찾아봤는데, 절차 지향 언어에서는 주로 main 함수 안에서 데이터(변수)들을 관리하고 데이터를 처리하기 위해 데이터 의존적인 함수를 만들어 호출한다고 한다.
  • 즉, 절차 지향 언어에서는 변수와 함수를 분리하여 관리 한다고 한다.
  • 즉, 모든 프로세스가 데이터에 의존적이라는 것이고, 이는 곧 데이터와 데이터를 처리하는 로직이 분리되어 있다는 것이다.
  • 그 것 뿐만 아니라 데이터를 처리하는 로직들(함수들), 데이터들 끼리도 다 묶여있지 않고 따로따로 분리되어 있다.
  • 반대로 객체 지향 언어는 데이터와 데이터를 처리하는 로직을 하나의 객체로 묶어서 관리한다.
  • 그렇다면 왜 절차 지향 언어는 데이터와 로직이 따로 존재하도록 설계를 할까?


  • 내 생각에는 일단 함수를 만들어 호출하는 구조는 실행 순서에 따라 프로그램의 흐름을 이해하기 쉽다는 장점이 있는 것 같다.
  • 또한 객체 지향 프로그밍에서는 데이터와 로직을 한 곳에 모은 객체들 간의 상호작용을 통해 프로그램이 실행되기 떄문에, 객체를 생성하고 메모리에 넣는 과정에서 시간이 소요되어서 실행 속도가 느릴 수 있다고 한다.
  • 이러한 이유 땜에 절차 지향 언어는 데이터와 로직을 분리해 함수들을 차례로 호출해 프로그램을 실행하는 구조로 설계하는 것 같다.
  • 하지만 절차 지향 언어는 프로그램이 커지고 복잡해질수록 코드의 관리가 어려워지고, 코드의 재사용성과 유지보수성이 높지 않다는 단점이 있다.
  • 반대로 객체 지향에서는 코드의 관리가 쉽고, 재사용성과 유지보수성이 뛰어나다.
  • 따라서 프로젝트의 요구사항에 맞게 절차 지향과 객체 지향 중 적절한 것을 선택하면 좋을 것 같다.

❓ 생성 로직?

  • 책에서 초기화 로직과 생성 로직이라는 말이 나온다.
  • 여기서 초기화 로직이 생성자를 이용해 초기화를 하는 로직이라고 생각했다.
  • 회원가입을 할 때 받는 포인트가 일반회원인지 프리미엄회원인지에 따라 달라지므로 회원을 생성할 때 첫 포인트를 초기화 해주는 로직이 초기화 로직이라고 생각했다.
  • 근데 그 뒤에 나오는 생성 로직도 같은 얘기를 하는 것 같다.
  • 같은 얘기를 그냥 다르게 말한 것인지, 아니면 생성 로직에 다른 의미가 있는 것인지, 아니면 내가 초기화 로직을 잘 못 이해한 것인지 스터디 시간에 같이 얘기해 보고 싶다.


  • 스터디에서 얘기해봤는데 다들 초기화 로직과 생성 로직을 같은 의미로 생각했다고 한다.
  • 책을 다시 읽어봐도 같은 의미가 맞는 것 같다.

party.members[memberId].equipments.canChange가 메서드 체인인가?

  • 책에서 메서드 체인의 예시로 나온 부분에서 members, equipments, canChange, armor 등은 메서드가 아닌 데이터 아닌가?
  • 메서드 체인은 메서드가 this 값을 리턴하고, 메서드들을 순서대로 실행시키는 것 아닌가?
  • 책의 예제는 그냥 Party 클래스의 인스턴스 party 내부의 members 중, 하나의 Member 클래스의 인스턴스 member 내부의, Equipments 클래스의 인스턴스 equipments 내부의, canChange나 armor라는 데이터에 접근하는 것 아닌가?
  • 이런것도 메서드 체인이라고 하는 것인가?


  • 메서드 체인은 주로 getter 메서드나 setter 메서드로 이루어 지는 것 같다.
  • 책에 나온 예시는 member, equipments, canChange에 접근하고 있는데 이것에 접근하려면 결국 getter 메서드를 부르는 것과 같은 작업이 일어난다고 생각하면, 메서드 체인이라고 볼 수도 있을거 같다는 얘기가 나왔다.
  • party.getMember(memberId).getEquipments().getCanChagne()와 같다고 생각하면, 메서드 체인이라고 볼수도 있나 라는 생각을 했다.
  • 확실하지는 않지만 이런 접근도 나쁘지 않은 것 같다.

results matching ""

    No results matching ""